《异教徒》数字人面部动画详解:如何用HDRP打造电影级实时渲染特效
制作一个逼真的数字人类是一项复杂的技术挑战,需要有海量的数据才能实现极高的图像水准。在制作《异教徒》时,Demo团队开发了许多的工具来克服各种问题:面部动画、毛发与皮肤的粘连,及眼睛、延迟和皮肤的渲染。所有工具目前已发布在GitHub上。本文将详细介绍制作方案中的完整技术细节。
我叫Lasse Jon Fuglsang Pedersen,是Unity Demo团队的高级软件工程师。在《异教徒》的制作中,我的一个主要任务是制作驱动数字人Gawain面部动画的技术方案。
该方案已在GitHub上作为单独资源包发布。而在本文中,我将讨论资源包的部分功能,分享一些功能的开发幕后。
面部动画
《异教徒》的数字人类在制作中有一个明确的目标:在保持整体写实手法的前提下,避免面部动画陷入“恐怖谷”。为了让动画尽可能匹配演员的动作表演,我们决定使用4D动作捕捉数据(一种逐帧3D扫描动画)来制作面部网格的动画。在捕捉面部表演时(在无遮拦的部位),4D数据至少可以保证网格的几何形是准确的。
使用4D捕捉数据带来了许多新的挑战,这部分动画导演Krasimir Nechevski已经在之前的博文中详细介绍过了(点击回看)。许多的精力都花在了数据的处理和调整、数据捕捉的实行,及做出满意成果这些方面上。
处理时的注意点
我们遇到的问题中,有一个关于眼睑几何形的问题。由于睫毛在捕捉时遮住了一部分眼睑,数据在这些位置上也会有缺失,形成一定的噪声干扰。因此,眼睑的几何形并不准确,会有抖动的现象,需要我们重新制作。
眼睑附近的几何形抖动
眼睑几何形的问题在制作早期就显现出来了。于是我们在导入数据到Unity时,在区域内使用了差异性网格处理技术,希望减少噪声干扰、重建几何形。具体来说,我们会抹平区域内的变化曲线来达到降噪效果,在捕捉序列的每一帧上,通过移植基本网格的曲线到带噪区域来重构几何形。
眼睑几何形的降噪和移植
虽然产出的几何形比较稳固,但和源数据相比人为合成的感觉有点更明显:眼睑虽然更加稳定,但也缺失了部分原动作,少了人的生气。很明显,我们需要寻求一种中庸效果,可惜时间过于短促,仔细研究是不可能的。自然而然地,我们请来一位外包来解决眼睑的重建工作。而GitHub上的资源包包括了原先用于降噪和网格移植的工具,作为学习资源可能会有一些价值。
匹配动画到皱纹
另一个问题是表面细节。由于目标网格的分辨率过低,表面细节有一定的缺失。Gawain的面部网格有约28000个顶点,但仍不足以将演员面部的皱纹用几何形展现出来,更别说表现皮肤毛孔的伸展了。虽然原始4D数据有一些细节,为了让网格的顶点符合预算,我们在变形、渲染网格时舍弃了这些细节。我们考虑过在每帧上烘焙一张法线贴图,但这样会占用不少宝贵的磁盘空间。
为了处理表面细节,我们决定尝试将导入序列的几何形状与来自Snappers Systems的基于混合形状的面部绑定的姿态驱动特征图耦合。来自面部绑定的姿势驱动的特征图包含了在导入序列时缺失的表面细节,如皱纹和毛孔的伸展。我们的想法是,如果可以找出最接近4D帧的Blendshape组合,就能用权重来驱动面部贴图(忽略掉Blendshape的变形效果),制作出4D回放时的表面细节。
将Blendshape匹配到4D分为两步。第一步是将问题部分以矩阵的方式列出,使用最小二乘法将所有的Blendshape(基础网格的增量)填入矩阵A,数据的每一列都储存了单个Blendshape的增量,而组合起来的增量用Ax = b表示,x表示单个BlendShapes的权重。
由于A不能逆算(这里数值并不是方阵),x的数值经常不能被解算出来。不过如果将问题以另一种算式表示出来,就能取得一个近似解x*。我们使用了正态方程ATAx*=ATb,将最小平方算式写作x*=(ATA)-1ATb,这时A只需有独立的线性数列即可。而在使用Blendshape时,我们需要筛选出已包含的形状,确保其能线性独立,然后就能求出一个近似值:我们预先计算出Blendshape骨架的(ATA)-1AT 值,然后为每帧的4D数据插入增量b值,来计算x*(拟合后的权重)。
虽然上方的最小平方方法用来理解问题是很不错,但实际使用就没那么好了。为了更接近4D帧的数值,方案中有时会含有负权重。但面部骨架仅允许Blendshape的值增加,不能减少,让拟合后的权重能突破骨架的限制,于是方案并不能总是将数值转化成有效的皱纹。
话句话说,我们需要使用一种无负数的方案来取得正确的皱纹。在计算无负数的方案时,我们使用了第三方库Accord.NET的子集,其中包含了一个专门计算无负数最小平方的可迭代结算器。在拆解问题、测试最小平方方案后,我们就求得了过滤后的Blendshape矩阵A和增量b,这时直接插入解算器来取得一个每帧拟合权重的非负数集。
部分前额匹配皱纹运算的前后对比
顺便一提,我们还尝试了根据网格边缘长度和边缘曲线来计算拟合权重的方法。如果不能移除4D数据的头部动作,我们本需要使用这些方法来让拟合效果独立于头部动作。我们最终在Gawain身上拟合了位置增量,而另两个方法也保存在了资源包中。
Unity中的工作流
在导入4D数据到Unity之前,我们首先依赖外部工具将数据转化成了一个网格序列(储存为.obj文件),在每帧上匹配拓扑。同时拓扑还需要匹配目标网格(详见Krasimir Nechevsky 的博文,点击回看)。
接着,资源包中包含有一个自定义类型的资源,称作SkinDeformationClip(皮肤变形片段),储存了预处理的4D数据和可用于运行时的片段。SkinDeformationClip在创建后会提供导入工具(及可选处理项),可用于导入部分4D数据,路径既可设为磁盘上所有的.obj文件(省去了在项目中导入整合性资源的必要)也能设为项目中已存有的网格资源。
从.obj文件中导入4D帧、制作片段
在配置好SkinDeformationClip资源后,点击检视器中的Import按钮就能开始导入、处理帧数据。注意,如果资源设定中同时启用了网格处理、帧拟合功能,导入过程会耗费不少时间。在导入完成后,片段将储存导入后的帧间隔、拟合权重等等数据,但并没有最终的帧数据。帧数据储存在另一个二进制文件中,以在播放时高效地完成数据传输。
在导入完成后,就能将资源拖到Unity Timeline的自定义时间轴SkinDeformationTimeline上进行播放。该时间轴能取得专门的SkinDeformationRenderer组件,将其作为片段数据的时间轴输出。下方视频展示了在Timeline上制定序列和播放4D数据的流程。
使用Unity Timeline播放片段资源
在使用自定义轴和SkinDeformationRenderer时,你也能将多个4D片段混合起来,使用这些数据进行艺术创作。在《异教徒》的第一部分,我们只使用了4D数据的一个小片段,其中仅包含了一个测试时首次近距离拍摄的3秒表演。不过,通过重复利用(通过剪切、缩放和混合),同个片段被用在了整个第一部的面部动画上。
皮肤附着系统
由于我们选择直接使用4D数据制作面部动画,无法依靠骨骼权重蒙皮或Blendshape来表现睫毛、眉毛或胡茬这类稍显次要的面部特征动画,需要用一种方法在面部网格上为这些面部部位添加动画。
技术上来说,我们能将处理后的4D数据加载到一个外部工具中,为网格建立模型、添加次要部位,然后烘焙出所有的额外数据。然而,从磁盘空间的角度来说,传输每帧的上万个额外顶点是无法实现的,并且产出结果也不会有动态的感觉。我们很明确4D数据在制作中需要迭代多次,因此在迭代时方案不能有冗长的烘焙步骤。
为了解决这个问题,我们为数字人类包添加了一个称为皮肤粘连系统的功能。系统允许将任意网格和变换粘连到一个指定的目标网格上,在运行时融合进目标网格,粘连的内容独立于目标网格的动画而存在。
在《异教徒》的数字人类身上我们使用了皮肤粘连系统来驱动眉毛、睫毛、胡茬及其它皮肤相关的印记。我们也使用了系统将皮草附着到了夹克上,这一点我们的3D艺术家Plamen Tamnev已经详细介绍过了(点击回看)。
下方是使用系统粘连网格的步骤,比如我们将一个Game Object的变化看作Gawain的脸:
放置、粘连一个变换
工作原理
当点击Attack键来附着一个变换时,系统将使用变换的位置来向k-d树查询最接近的网格顶点。顶点随后用于识别所有的三角形,系统会针对每个三角形根据变换当前位置来生成一个局部姿态,形成一套变换的局部姿态。
最近顶点在三角形上的投射
每个局部姿态都是一个三角形附着点的投射,并且还包含了三角形的顶点索引值、附着物到三角形的法线距离,以及投射点的质心坐标。
变形完成后,系统将分别去除投射点,平整网格
我们之所以为每个附着点生成多个局部姿态,而不是在三角形上生成姿态,是为了支持部分不属于三角形的点。比如部分悬浮在网格上方的毛发图片。为了在多个局部姿态上解决粘连点的问题,我们首先需要分别删去每个三角形的局部姿态,然后平整三角形的较高处。
在生成后,局部姿态将储存在一个巨大的数组中,数组还包括了面部所有粘连的局部姿态。每个附着物将引用数据、检查数据总量,防止底层数据因其他原因被修改。
粘连一块网格和粘连变换类似,只是流程会多重复几次。在粘连网格时,系统会为每个顶点生成一套局部姿态集,而不是单个的变换位置。
在普通网格模式下粘连眉毛
网格上同样有一个称为MeshRoots的次级粘连模式:在该模式下,系统首先根据相关性将网格划分为网格区,再找出每个区相对于面部网格的“根”。最后,系统会让单个区保持刚性状态。举例来说,睫毛就是以这种方法粘连的,而眉毛不是。这是因为眉毛的毛发图片会随皮肤运动,需要有变形效果,而睫毛的毛发图片会保持自己的形状。
附着到根网格上的独立睫毛区
在运行时,系统会抓取附着对象的位置和顶点(网格的变换),持续更新数据来使其融合进面部网格。每一帧,系统会计算面部网格的状态输出,将其与现有局部姿态相组合,解算出所有皮肤相关的位置和顶点。下方图像展示了Gawain身上密集的附着效果。
借助面部网格解算出的点
运行时的解算过程借助了C# Job System和Burst Compiler加速,让处理数据量更加庞大。举例来说,Gawain面部的解算需要在每帧上解算上万个局部姿态,来生成面部细微特征的动画。
着色器与渲染
当我们开始制作数字人类的单独资源包时,一个主要的目标是将所有渲染相关的内容转换为原始的高清渲染管线(HDRP)内容,确保包能使用新的HDRP功能来升级、拓展。
背景:当我们开始制作《异教徒》的原型图像时,HDRP仍缺少部分拓展性相关的功能。我们也还不能编写自定义可升级着色器,不能在帧之间插入自定义渲染命令(如自定义渲染通道)。
因此,数字人类(以及影片中部分其它效果)的自定义着色器在建立原型时属于HDRP材质的直接分支。管线在当时仍处于预览阶段,有很多的结构性改动。许多自定义着色器还需要修改HDRP的核心,使得升级愈发困难。我们需要搜寻更多的HDRP拓展功能,减少自定义的需要。
于是,制作数字人类资源包需要将原先必要的自定义设定转换为现今HDRP拓展而来的功能:把自定义着色器转换为Shader Graph,使用HDRP专用的主节点,借助CustomPass API来执行必要的自定义渲染通道。这里需要感谢一下Unity的首席图形工程师Sebastien Lagarde和Unity Hackweek 2019上的一支团队,他们为HDRP加入了Eye Master眼睛主节点。节点可兼容《异教徒》的自定义功能,在导入眼睛时发挥了巨大作用。
在下一节我将介绍皮肤、眼睛和牙齿的Shader Graph。资源包内还有一个头发的Shader Graph,是一个直通Hair主节点的单路径式图表。
皮肤
在整体上,皮肤着色器大幅依赖HDRP内置的次表面散射功能。功能可让艺术家们为各类材质制作、指定不同的散射配置,形成包括各类皮肤的仿真效果。皮肤着色器使用StackLit主节点,以形成两个反射片(一种常用于制作皮肤的方法,不支持Lit主节点),因此皮肤着色器的渲染仅能是前向。
皮肤着色器的Shader Graph
与普通受光着色器一样,这两个反射片上的皮肤平滑度都由遮罩贴图生成,而第二个平滑度则在材质检视器中作为可调整的常量被暴露出来。美术们同样可以用遮罩贴图控制环境光遮蔽,和两张细节贴图的影响力程度。两张图分别为细节法线贴图和细节平滑贴图,平滑贴图会同时影响主要和次级平滑度。
除了普通的遮罩贴图,皮肤着色器同样能接收凹陷贴图(Cavity Map,单通道纹理,在凹陷处有较低的数值),用以控制小凹陷处,如皮肤毛孔上的反射剔除因素和/或平滑度。凹陷贴图的效果也可在掠角上选择性地消除,来模仿掠角中凹陷处被隐藏的效果。
使用凹陷贴图修改毛孔上的平滑度
皮肤着色器还支持由Snapper面部骨架驱动的面部特征(如皱纹)。在皮肤的着色器图表中,功能被封装在一个自定义节点中,节点的部分输入本身并不能在图表中查看。这些隐藏起来的输入由SnappersHeadRenderer组件驱动,组件则需要作为SkinnedMeshRenderer储存在使用着色器的GamObject上。
拟合后的权重在皮肤着色器上被转换成皱纹
另一个有意思的节点是泪线相关的图表,这部分将在眼睛部分之后解释。基本上来说,为了让泪线效果能修改皮肤的法线,我们需要在深度Prepass时计算、储存法线,然后在前向通道中重新采样(如果重新运算法线,会导致所有中间处理都被舍弃)。
眼睛
《异教徒》的自定义眼睛着色器是与软件工程师Nicholas Brancaccio合作制作的。Nicholas参与了着色器最初的制作,包括分成两层的光照模型,以及眼睑附近光照遮蔽的计算函数。
眼睛着色器的Shader Graph
着色器为眼睛建立了一个双层材质的模型。在模型上,第一层用于表现角膜和表面的液体,而第二层用于表现巩膜和虹膜。光线会分别分布在两层上:上层主要计算镜面反射(表现角膜和表面液体的模型层,其表面更加光滑),而下层仅计算光照散射(虹膜和巩膜)。
在光源下眼球的无遮拦视图
角膜上的折射会于眼球内处理完毕,效果依赖于几何形输入和一些可变更的参数。眼球几何形的输入需为仅描述眼球表面(包括角膜突起)的单个网格。
接着,在截面(粗略)描述出角膜的起点后,我们便能在渲染时识别出该区域是否为角膜的一部分。如果是,则折射光线,使光线在表示虹膜的平面上互相交叉。虹膜平面可经由角膜界面上的偏移来调整,允许艺术家调整眼球上并行图像的数量。
旋转眼球时出现的角膜反射
为了算出虹膜中的光照散射程度,眼球着色器带有一种根据表面(角膜)光栅化片段,将入射光照折射向虹膜的选项。虽然功能并不能生成精确的焦散效果(仅会在反射表面上接收接收、积累单个片段投来的光),但在受光时,比如从侧边观察时,虹膜至少不会在影子中不自然地凸显出来。光照反射功能已成为眼睛主节点的一部分,可在Eye Cinematic模式中启用。
反射照向虹膜的射入光线
我们使用了一种球型高斯各向异性算法来完成眼睑附近的遮蔽。有四个标记(变换)会借助皮肤附着系统追踪眼睑,完成遮蔽效果的分布。具体来说,我们使用两个标记来追踪眼球的边角来形成一个封闭的轴,其余两个标记则追踪上下两片眼睑,用于推测眼睑的闭合角。封闭的轴与闭合角接着会生成测算球型高斯各向异性所需的必要矢量。我们使用测算结果直接作为眼球主节点上的环境光和镜面反射遮蔽的因子,有时还用于选择性地修改albedo来人为降低遮蔽区域的亮度。
驱动球型高斯各向异性遮蔽的四个标记物
在眼睛图表中,包括角膜折射和眼睑遮蔽在内的大部分功能都使用了一个自定义功能节点:EyeSetup,可向图表输出一系列可读数据。类似于皮肤图表面部骨架上的自定义功能节点,该节点使用了一个隐藏的参数,不能在材质检视器中修改,仅能通过脚本代码控制。这时因为参数十分复杂,需要在每帧上抓取。而在眼睛图表中,隐藏参数由EyeRenderer组件驱动。而为了让着色器能生成正确的结果,组件同样需要作为渲染器放置在同一个GameObject下。
EyeRenderer组件除了会计算、传入数值到着色器,还提供有协助建立眼睛模型的实用工具。比如,用户可使用工具来可视化、调整角膜区域的截面,或者检视、微调平面投射纹理的前向轴,防止眼球几何形未能正对z轴。
在场景视图中拖动控点来调整眼球
最后,眼睛图标和皮肤图表类似,也有一个整合泪线的节点:节点会在深度前通道中写入法线和平滑度,在前向通道中再次采样。
泪线
为了重现泪线(眼睛与皮肤间的潮湿带),我们使用了HDRP自定义API,在帧的特定阶段插入自定义渲染。
通过使用自定义通道来控制HDRP法线缓存上的内容(其中包含了法线贴图和平滑度数据),我们在面部的特定屏幕空间上模糊了法线贴图和平滑度(比如在眼睛与皮肤的接触位置)。由于皮肤和眼睛为前向材质,我们还需要在图表中插入特定的节点来在前向通道中采样。
应用泪线模糊通道前后的法线贴图
在法线缓存中添加平滑的过渡效果可让两个表面很好地相连接。如果与较高的平滑度数值相结合,就能在两个材之间产生一种镜面高光效果,让泪线能有潮湿的感觉。
添加泪线前后的着色结果
为了标记出模糊区域,我们使用了一种简单的遮罩贴花,放置在特定层上,并且不渲染任何颜色(调试时除外)。有了这些特殊层上的贴花,我们就能更轻松地用自定义通道筛选、渲染,只需要设定HDRP中的自定义模板字位即可。当所有遮罩贴花储存进模板后,我们就有一个指示模糊区域的屏幕空间遮罩。该遮罩还能动态地将模糊通道缩小到遮罩的边缘,防止区域的边缘出现模糊。
显示出泪线遮罩贴花的调试覆盖视图
在制作Gawain的泪线时,我们为每个眼睛制作了专门的遮罩贴花,在无表情时覆盖住的眼睑和眼球,然后使用皮肤附着系统粘连到皮肤上。为了做出眼球和眼睑的小缝隙(4D数据中比较明显),我们还稍微扩大了些几何形,使其与眼球能有向内的覆盖图像。
牙齿
牙齿着色器使用了受光主节点的许多功能,包括此表面散射和透光表层遮罩。除了使用受光节点现有的功能外,着色器还使用了一种自定义衰减效果,用以根据嘴巴当前的张开成程度,平滑地使口腔内逐渐变暗。
牙齿着色器的Shader Graph
为了测算出目前口部张开程度,我们在嘴唇上放置了6个标记,形成一个近似与嘴唇内部弯曲的多边形。在Gawain身上,我们使用了皮肤附着系统来驱动标记,使其无视面部网格的动画来跟随嘴唇的变化。
在渲染时,我们首先将多边形传给着色器,然后将其投射到当前的不受光半球形网格上,形成一个球体。该球体能直观地展示出从当前片段的角度,透过嘴部开口能看见多少内容。
在嘴内用一个球体可视化球型多边形
应用自定义衰减的前后效果
与皮肤和眼睛图标类似,牙齿图表同样包含一个隐藏的自定义功能节点。隐藏的参数由TeethRenderer组件提供,需要作为渲染器添加在使用该着色器的GameObject上。
后记
希望本文能阐述出制定Gawain面部技术方案的挑战和我们付出的努力。
如果想要尝试这些工具,或在此基础上制作自己的工具,可以在GitHub上下载代码库、在自己的项目(包括商业项目)中使用。我们期待看到大家的创作!
《异教徒》制作揭秘系列(点击文章标题,直达原文):
文中提及的相关链接:
[1] 《异教徒》链接:
https://unity.cn/the-heretic
[2] GitHub 链接:
https://github.com/Unity-Technologies/com.unity.demoteam.digital-human
[3] Accord.NET 的子集链接:
http://accord-framework.net/
[4] Unity Timeline 文档:
https://docs.unity3d.com/Packages/com.unity.timeline@1.5/manual/tl_about.html
[5] C# Job System 文档:
https://unity.com/dots/packages#c-job-system
[6] Burst Compiler 文档:
https://unity.com/dots/packages#burst-compiler
[7] 高清渲染管线(HDRP):
https://unity.cn/srp/High-Definition-Render-Pipeline
[8] CustomPass API 文档:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@7.1/manual/Custom-Pass.html
[9] Hair 主节点文档:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@7.3/manual/Master-Node-Hair.html
[10] HDRP内置的次表面散射功能文档:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@9.0/manual/Subsurface-Scattering.html
[11] HDRP中的自定义模板字位文档:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@7.3/api/UnityEngine.Rendering.HighDefinition.UserStencilUsage.html
每一个“在看”,都是我们前进的动力